using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Runtime.Serialization;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;


namespace Rogue
{
    [Serializable]
    public class Entity : ISerializable
    {
        #region Position Members & Accessors
        private Point m_pRawPosition;       // no collector update - use Position to keep the EntityCollector updated.
        public Point Position
        {
            get { return m_pRawPosition; }
            set { MoveTo(value);  }
        }

        public int X
        {
            get { return m_pRawPosition.X; }
            set { MoveTo(new Point(value, Y));  }
        }

        public int Y
        {
            get { return m_pRawPosition.Y; }
            set { MoveTo(new Point(X, value)); }
        }

        private static EntityCollector m_Collector = new EntityCollector();
        public static EntityCollector Collector
            { get { return m_Collector; } }
        #endregion

        protected string m_sName;
        public string Name
            { get { return m_sName; } }

        #region time and updates
        protected int m_nNextThink;
        public int NextThink
        { 
            get { return m_nNextThink; }
            set { m_nNextThink = value; }
        }

        protected int m_nDelay;
        public int Speed
            { get { return m_nDelay; } }

        protected bool m_bPlayerControlled;
        public bool IsPlayerControlled
        { 
            get { return m_bPlayerControlled; }
            set 
            {   
                if (value == m_bPlayerControlled)
                    return;

                if (value == true)
                {
                    InputManager.Instance.OnKeyPressed += new InputManager.OnKeyPressedHandler(OnKeyPressed);
                    InputManager.Instance.OnGamePad += OnGamePad;
                }
                else
                {
                    InputManager.Instance.OnKeyPressed -= OnKeyPressed;
                    InputManager.Instance.OnGamePad -= OnGamePad;
                }

                m_bPlayerControlled = value; 
            }
        }
        #endregion

        #region inventory
        protected List<Weapon> m_Items;
        public List<Weapon> Items
        { get { return m_Items; } }

        public virtual bool CanPickUp(Entity a_Item)
        {
            Weapon w = a_Item as Weapon;
            return (w != null) && (w.Owner == null || !w.Owner.IsAlive);
        }

        #endregion

        #region life and health
        protected bool m_IsAlive;
        public bool IsAlive
        { 
            get { return m_IsAlive; }
            set { m_IsAlive = value; } 
        }
        

        protected BodyPart m_BodyRoot;
        public BodyPart BodyRoot
        { get { return m_BodyRoot; } }

        private float m_nHealth = 100;      // used if we don't have a bodysetup
        public float Health
        { 
            get 
            {
                if (m_BodyRoot != null)
                    return m_BodyRoot.GetTotalHealth();
                else
                    return m_nHealth;
            } 
        }



        #endregion


        public virtual void Think()
        {
            // For overriding!
            EndTurn();
        }

        public void ResetTicksUntilThink()
        {
            m_nNextThink = m_nDelay;
        }

        // default naming constructor
        public Entity()
        {
            m_sName = String.Format("{0}", GetType().Name);
            Init();
        }

        public Entity(string a_sName)
        {
            if (a_sName.Length > 0)
            {
                m_sName = a_sName;
            }
            else
            {
                m_sName = String.Format("{0}", GetType().Name);
            }
            Init();
        }

        public virtual void Init()
        {
            EntityDef EntDef = EntityDatabase.Instance.GetDefinition(this.GetType().Name);
            m_pRawPosition = new Point(0, 0);
            m_nHealth = EntDef.Health;
            m_nDelay = EntDef.Speed;
            m_IsAlive = true;

            OnMove += new MoveHandler(Entity_OnMove);

            m_Items = new List<Weapon>();
        }

        void Entity_OnMove(Entity.MoveArgs e)
        {
            Collector.Update(this, e.m_oldPos, e.m_newPos);
        }

        #region damage and fighting

        static Attack s_DefaultAttack = new Attack("push", "pushes", "", 1, DamageType.Bash, Attack.Priority.LastResort);

        public List<Attack> GetAttacks(bool a_HighestPriorityOnly)
        {
            // todo: collect attacks from other sources?
            // Eg. from inventory, from spells, etc.
            // How do we let user input hook into this? Selecting an attack in special cases...

            List<Attack> availableAttacks = new List<Attack>();
            availableAttacks.Add(s_DefaultAttack);

            foreach (BodyPart part in m_BodyRoot.GetAllParts<BodyPart>())
            {
                if (part.GrantedAttack != null && !availableAttacks.Contains(part.GrantedAttack))
                    availableAttacks.Add(part.GrantedAttack);
            }

            foreach (Entity e in Items)
            {
                Weapon w = e as Weapon;
                if (w != null && w.GrantedAttack != null && !availableAttacks.Contains(w.GrantedAttack))
                    availableAttacks.Add(w.GrantedAttack);
            }

            // sort the attacks by priority, then pick a random high-priority attack
            // TODO: attack picking once we could do multiple ones
            availableAttacks.Sort(delegate(Attack a, Attack b) { return (int)b.m_Priority - (int)a.m_Priority; });

            if (a_HighestPriorityOnly)
            {
                Attack.Priority bestPriority = availableAttacks[0].m_Priority;
                availableAttacks.RemoveAll(delegate(Attack x) { return x.m_Priority != bestPriority; });
            }
            
            return availableAttacks;
        }

        protected Attack m_SelectedAttack = null;

        private void SelectAttack(int p)
        {
            List<Attack> attacks = GetAttacks(false);

            if (p < 0 || p >= attacks.Count)
            {
                m_SelectedAttack = null;
                return;
            }

            m_SelectedAttack = attacks[p];
        }

        public virtual Attack GetSelectedAttack()
        {
            return m_SelectedAttack;
        }

        public virtual Attack GetAttack()
        {
            List<Attack> allAttacks = GetAttacks(false);

            if (m_SelectedAttack != null && allAttacks.Contains(m_SelectedAttack))
                return m_SelectedAttack;
            else
                m_SelectedAttack = null;

            List<Attack> bestAttacks = GetAttacks(true);

            // pick it for them
            if (IsPlayerControlled)
            {
                m_SelectedAttack = bestAttacks[Random.Instance.Next(bestAttacks.Count)];
            }

            return bestAttacks[Random.Instance.Next(bestAttacks.Count)];
        }

        public virtual void MeleeAttack(Entity a_Other)
        {
            MeleeAttack(a_Other, null);
        }

        public virtual void MeleeAttack(Entity a_Other, BodyPart a_SelectPart)
        {
            Attack attack = GetAttack();

            // Output stuff. TODO: put this in a battle reporting class.
            string attackerName = null;
            string victimName = null;
            string verb = null;
            if (IsPlayerControlled)
            {
                if (IsPlayerControlled)
                    attackerName = "You";
                else
                    attackerName = String.Format("You ({0})", Name);
                
                victimName = String.Format("the {0}", a_Other.GetType().Name);
                verb = attack.m_YouVerb;
            }
            else
            {
                attackerName = String.Format("The {0}", GetType().Name);
                victimName = "you";
                verb = attack.m_HeVerb;
            }

            int cost = GetAttackDelay();

            if (a_Other.BodyRoot == null)       // will we allow no body setup?
            {
                ConsoleWindow.Instance.AddMessage(String.Format("{0} {1} the {2} for {3} points of damage!", 
                                                attackerName, verb, a_Other.GetType().Name.ToLower(), attack.m_BaseDamage),
                                        Color.Green);

                a_Other.TakeDamage(attack, null, this);
            }
            else
            {
                BodyPart missedPart = null;
                if (a_SelectPart != null)
                {
                    cost = (int) (cost * 1.5f);   // targeting parts is slow

                    float rand = Random.Instance.Next();
                    float cth = a_Other.GetChanceToHit(a_SelectPart);
                    if (cth < rand)
                    {
                        missedPart = a_SelectPart;
                        a_SelectPart = null;
                    }
                }

                if (a_SelectPart == null)
                {
                    List<BodyPart> targets = new List<BodyPart>(a_Other.BodyRoot.GetAllParts<BodyPart>());
                    a_SelectPart = targets[Random.Instance.Next(targets.Count)];     // weight by size or something?

                    if (missedPart == a_SelectPart)
                    {
                        // you missed the part you were aiming for and accidentally hit it. k.
                        a_SelectPart = missedPart;
                        missedPart = null;
                    }
                }

                a_Other.TakeDamage(attack, a_SelectPart, this);

                string message = null;
                if (missedPart != null)
                {
                    message = String.Format("{0} miss the {1}, and {2} {3} in the {4}. It is {5}.", 
                        attackerName, missedPart.GetLocationAndName().ToLower(), verb, victimName, a_SelectPart.GetLocationAndName().ToLower(), a_SelectPart.DescribeCurrHealth());
                }
                else
                {
                    message = String.Format("{0} {1} {2} in the {3}. It is {4}.", 
                        attackerName, verb, victimName, a_SelectPart.GetLocationAndName().ToLower(), a_SelectPart.DescribeCurrHealth());
                }
                ConsoleWindow.Instance.AddMessage(message, a_SelectPart.GetColorFromHealth());
            }

            ConsoleWindow.Instance.ProcessPostDamageMessages();

            if (!a_Other.IsAlive)
            {
                a_Other.PrintDeathMessage();
            }

            EndTurn(cost);
        }

        public virtual void TakeDamage(Attack a_Attack, BodyPart a_Where, Entity a_Attacker)
        {
            bool wasAlive = IsAlive;
            
            if (a_Where != null)
            {
                a_Where.TakeDamage(a_Attack);
                // isAlive should get set false in a callback here...
            }
            else
            {
                m_nHealth = Math.Max(0, m_nHealth - a_Attack.GetTotalDamage());
                m_IsAlive = m_nHealth > 0;
            }

            if (OnDamage != null)
                OnDamage(new DamageArgs());

            if (wasAlive && !IsAlive)
            {
                Die();

                if (Speed != -1)
                    RogueGame.Instance.Map.BloodyTerrain(X, Y, true);
            }

            if (Speed != -1 && a_Attack.m_Type == DamageType.Cut && Random.Instance.Next(10) > a_Attack.m_BaseDamage)
                RogueGame.Instance.Map.BloodyTerrain(X, Y, true);
        }

        public virtual void Die()
        {
            if (OnDeath != null)
                OnDeath(new DeathArgs());
            Timeline.Instance.Remove(this);
        }

        // override these for your base class.
        // TODO: automatically get the class name (given the type?) and load all the images possible?
        static DetailWindowImages s_DefaultDetailWindowImages = new DetailWindowImages("default");
        public virtual DetailWindowImages GetDetailWindowImages()
        {
            return s_DefaultDetailWindowImages;
        }

        // replace this with a battle-reporting system or something...
        public virtual void PrintDeathMessage()
        {
            ConsoleWindow.Instance.AddMessage(m_sName + " has been slain!", Color.Red);
        }

        #endregion

        #region movement and obstruction
        public virtual bool BlocksActor(Entity a_WantsToWalkThroughMe)
        {
            return IsAlive;
        }

        public virtual bool BlocksLight()
        {
            return IsAlive;
        }



        public bool IsCriticallyWounded()
        {
            if (BodyRoot != null && Speed != -1)
            {
                foreach (BodyPart b in BodyRoot.GetAllParts<BodyPart>())
                {
                    if (b.GetHealth() < b.GetMaxHealth() * 0.5f)
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        // updates our position in the entity collector, move inventory with us, etc
        public void MoveTo(Point a_newPos)
        {
            Point oldPos = m_pRawPosition;

            m_pRawPosition = a_newPos;

            if (OnMove != null)
                OnMove(new MoveArgs(oldPos, a_newPos));

            
        }

        public virtual void MoveDelta(Direction a_Dir)
        {
            Point pNewPos = m_pRawPosition + a_Dir;

            bool blocking = false;
            foreach (Entity blocker in Entity.Collector.GetAllAt<Entity>(pNewPos.X, pNewPos.Y))
            {
                if (blocker.BlocksActor(this))
                    blocking = true;
            }

            if (blocking)
            {
                // anything to do here? Tell them to move?
            }
            else if (!RogueGame.Instance.Map.BlocksActor(pNewPos.X, pNewPos.Y))
            {
                Position = pNewPos;
                EndTurn(GetMoveDelay());               
            }
        }

        // will return null if it's nothing special.
        public virtual string DescribeMoveSpeed()
        {
            int delay = GetMoveDelay();

            if (delay > m_nDelay * 4)
                return "crippled";
            if (delay > m_nDelay * 2)
                return "limping";

            // modify this, I have no idea what reasonable speeds are... these are delays, not speeds.
            if (delay > 300)
                return "very slow";
            if (delay > 200)
                return "slow";
            if (delay > 100)
                return null;
            if (delay > 50)
                return "fast";

            return "very fast";
        }

        public float GetChanceToHit(BodyPart p)
        {
            float partChance = p.GetMaxHealth() / 50.0f;

            if (GetMoveDelay() == -1)
                return Math.Min(1.0f, partChance);

            return Math.Min(1.0f, GetMoveDelay() / 100.0f * partChance);
        }

        public virtual int GetMoveDelay()
        {
            return m_nDelay;
        }

        // todo: change into a property of the attack
        public virtual int GetAttackDelay()
        {
            return m_nDelay;
        }

        public void EndTurn()
        {
            EndTurn(m_nDelay);
        }

        public void EndTurn(int a_Delay)
        {
            m_nNextThink += a_Delay;
            Timeline.Instance.Remove(this);
            Timeline.Instance.Add(this);
        }
        #endregion

        #region Input Processing
        public virtual void OnGamePad(Object a_Sender, InputManager.GamePadEventArgs a_Args)
        {
            if (Timeline.Instance.CurrentEntity != this)
                return;

            if (!IsAlive)
                return;

            // does the gamepad want us to move?
            GamePadState padState = a_Args.m_CurGamePadState;
            const float minInputStrength = 0.5f;
            float inputStrength = padState.ThumbSticks.Left.Length();
            Direction dir = null;
            if (inputStrength > minInputStrength)
            {
                dir = Direction.GetDirection( (int) Math.Sign(padState.ThumbSticks.Left.X),
                                             -(int)Math.Sign(padState.ThumbSticks.Left.Y));
            }

            if (dir == null)
            {
                dir = Direction.GetDirection((int)padState.DPad.Right - (int)padState.DPad.Left, 
                                             (int)padState.DPad.Down - (int)padState.DPad.Up);
            }

            if (dir != null)
            {
                MoveOrAttack(dir);
                return;
            }
        }

        public virtual void OnKeyPressed(Object a_Sender, InputManager.KeyEventArgs a_Args)
        {
            if (!IsAlive)
                return;

            Timeline.Instance.AdvanceToPlayer();

            // check again - that may have killed him
            if (!IsAlive)
                return;

            bool selectPart = a_Args.Control;
            switch (a_Args.Key)
            {
                case Keys.NumPad0:
                case Keys.NumPad5:
                    EndTurn(50);    // half a sec?
                    break;

                case Keys.P:    // pick up
                    PickUp();
                    break;

                #region attack selection
                case Keys.F1:
                    SelectAttack(0);
                    break;

                case Keys.F2:
                    SelectAttack(1);
                    break;

                case Keys.F3:
                    SelectAttack(2);
                    break;

                case Keys.F4:
                    SelectAttack(3);
                    break;

                case Keys.F5:
                    SelectAttack(4);
                    break;

                case Keys.F6:
                    SelectAttack(5);
                    break;

                case Keys.F7:
                    SelectAttack(6);
                    break;
                #endregion


                #region movement
                case Keys.NumPad8:
                    MoveOrAttack(Direction.North, selectPart);
                    break;

                case Keys.NumPad9:
                    MoveOrAttack(Direction.Northeast, selectPart);
                    break;

                case Keys.NumPad6:
                    MoveOrAttack(Direction.East, selectPart);
                    break;

                case Keys.NumPad3:
                    MoveOrAttack(Direction.Southeast, selectPart);
                    break;

                case Keys.NumPad2:
                    MoveOrAttack(Direction.South, selectPart);
                    break;

                case Keys.NumPad1:
                    MoveOrAttack(Direction.Southwest, selectPart);
                    break;

                case Keys.NumPad4:
                    MoveOrAttack(Direction.West, selectPart);
                    break;

                case Keys.NumPad7:
                    MoveOrAttack(Direction.Northwest, selectPart);
                    break;
                #endregion
            }

            if (IsAlive)
            {
                // Are we pre-healing here (ie. giving you health for the damage you just took in this turn?)
                // Should the timestep be m_nNextThink - m_nLastThink?
                // Should also go in some more generic spot, not in input handling!

                int elapsedTime = m_nNextThink - Timeline.Instance.CurrentTime;
                m_BodyRoot.UpdateSubpartsHealth(elapsedTime);

                // did that healing just kill him?
                if (!IsAlive)
                {
                    // TODO: have the battle reporter thing handle this
                    ConsoleWindow.Instance.AddMessage(String.Format("{0} has bled to death.", Name), Color.Red);
                    Die();
                }
            }
        }

        public void MoveOrAttack(Direction a_Dir)
        {
            MoveOrAttack(a_Dir, false);
        }

        public bool CanAttack(Entity e)
        {
            return this != e && IsAlive && e.IsAlive && e.OnPlayerBump == null && Math.Abs(e.X - X) <= 1 && Math.Abs(e.Y - Y) <= 1;
        }

        // should be used by the player only, since AIs should do this differently...
        public void MoveOrAttack(Direction a_Dir, bool a_SelectPart)
        {
            Point pNewPos = new Point(Position.X + a_Dir.DX, Position.Y + a_Dir.DY);

            Entity bumpable = null;
            Entity walkingInto = null;
            foreach (Entity blocker in Entity.Collector.GetAllAt<Entity>(pNewPos.X, pNewPos.Y))
            {
                if (blocker.OnPlayerBump != null)
                {
                    bumpable = blocker;
                    walkingInto = blocker;
                    break;
                }

                if (blocker.BlocksActor(this))
                {
                    walkingInto = blocker;
                }
            }

            if (IsPlayerControlled && bumpable != null)
            {
                bumpable.OnPlayerBump(new BumpArgs());
            }
            else if (walkingInto != null && walkingInto.BlocksActor(this)) // && ShouldAttack?
            {
                if (a_SelectPart && DetailWindow.EnemyWindow.BodyPartToDamage == null)
                {
                    // take a 0-length turn. A little bit hax...
                    DetailWindow.EnemyWindow.Focus(walkingInto, true);
                    EndTurn(0);
                    return;
                }

                MeleeAttack(walkingInto, DetailWindow.EnemyWindow.BodyPartToDamage);
            }
            else
            {
                MoveDelta(a_Dir);
            }
        }

        public virtual void PickUp(Entity a_Item)
        {
            Weapon w = a_Item as Weapon;

            if (w != null && !m_Items.Contains(w))
            {
                w.Position = Position;
                OnMove += new MoveHandler(Inventory_OnMove);
                w.Owner = this;
                m_Items.Add(w);

                if (OnPickup != null)
                    OnPickup(new PickupArgs());
            }
        }

        public void PickUp()
        {
            // no UI for now, if there are multiple choices to make
            foreach (Entity e in Entity.Collector.GetAllAt<Entity>(X, Y))
            {
                if (CanPickUp(e))
                {
                    PickUp(e);
                    ConsoleWindow.Instance.AddMessage(string.Format("You pick up the {0}.", e.Name), Color.Yellow);
                    break;
                }
            }
        }

        void Inventory_OnMove(Entity.MoveArgs a_Args)
        {
            foreach (Entity e in Items)
                e.Position = Position;                
        }

        public void Drop(Entity a_Item)
        {
            Weapon w = a_Item as Weapon;
            if (w != null && m_Items.Contains(w))
            {
                Entity hero = RogueGame.Instance.Map.GetHero();
                bool found = false;
                foreach (Weapon weap in hero.Items)
                    if (weap.Name == w.Name)
                        found = true;
                
                if (!found)
                    ConsoleWindow.Instance.AddMessage(string.Format("{0} drops the {1}. Walk over it and press P to pick it up.", Name, w.Name), Color.Yellow);

                w.Owner = null;
                m_Items.Remove(w);
            }
        }

        #endregion

        public class MoveArgs : EventArgs 
        {
            public MoveArgs(Point oldPos, Point newPos)
            {
                m_oldPos = oldPos;
                m_newPos = newPos;
            }
            public Point m_oldPos;
            public Point m_newPos;
        };
        public delegate void MoveHandler(MoveArgs e);
        public event MoveHandler OnMove;

        public class BumpArgs : EventArgs { };
        public delegate void PlayerBumpHandler(BumpArgs e);
        public event PlayerBumpHandler OnPlayerBump;

        public class DeathArgs : EventArgs { };
        public delegate void DeathHandler(DeathArgs e);
        public event DeathHandler OnDeath;

        public class DamageArgs : EventArgs { };
        public delegate void DamageHandler(DamageArgs e);
        public event DamageHandler OnDamage;

        public class PickupArgs : EventArgs { };
        public delegate void PickupHandler(PickupArgs e);
        public event PickupHandler OnPickup;

        #region Serialization
        public Entity(SerializationInfo info, StreamingContext context)
        {
            m_sName = (string)info.GetValue("m_sName", typeof(string));
            m_pRawPosition = (Point)info.GetValue("m_pPosition", typeof(Point));
            m_nHealth = (int)info.GetValue("m_Health", typeof(int));
            //m_EntityType = (string)info.GetValue("m_EntityType", typeof(string));
        }

        public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("m_sName", m_sName);
            info.AddValue("m_pPosition", m_pRawPosition);
            info.AddValue("m_Health", m_nHealth);
            //info.AddValue("m_EntityType", m_EntityType);
        }
        #endregion

        public virtual string GetStatusDescription()
        {
            return DescribeMoveSpeed();
        }
    }
}






